from defined_exceptions import ParameterError

#moveDict has negative and postive numbers to easily specify which moves reverse which
moveDict = {'rightFlip' : -1, 'leftFlip' : -2, 'flipRight' : 1, 'flipLeft' : 2}
heuristicChoiceDict = {'lowestCircle' : 0, 'averageCircle': 1}
heuristicChoice = heuristicChoiceDict['lowestCircle']


def checkStateValidity(state):
    """Tests validity of state string and raises an exception if invalid
    
    state strings must be 18 characters long, contain nine hyphens, three 'O', and 
    one each of 'A', 'B', 'T', 'L', 'R', and 'G'
    """ 
    if len(state) != 18:
        raise ParameterError("state string is the wrong length.")
    hyphenCount = state.count("-")
    oCount = state.count("O")
    aCount = state.count("A")
    bCount = state.count("B")
    tCount = state.count("T")
    lCount = state.count("L")
    rCount = state.count("R")
    gCount = state.count("G")    
    if(hyphenCount!=9 or oCount!=3 or aCount!=1 or bCount!=1 or tCount!=1 
                      or lCount!=1 or rCount!=1 or gCount!=1):
        raise ParameterError("state string contains invalid characters or incorrect numbers of certain characters: \""+state + "\"")
#The following is no longer needed as the previous conditional automatically checks this
#     if not state.strip('-AOBGLTR'): # This has the effect of emptying the string when it only contains valid characters
#         raise ParameterError("state string contains invalid characters")

def printState(state):
    from sys import stdout
    def printChar(char):
        if char == "L":
            stdout.write(u"\u25E3")
        elif char == "R":
            stdout.write(u"\u25E5")
        elif char == "G":
            stdout.write(u"\u25E2")
        elif char == "T":
            stdout.write(u"\u25E4")
        elif char == "O":
            stdout.write(u"\u25CF")
        elif char == "-":
            stdout.write(u"\u25A1")   
        else:
            stdout.write(char)
            
            
    checkStateValidity(state)
    
    stdout.write(" "*4)
    for i in range(2):
        printChar(state[i])
    printChar("\n")
    for i in range(2,9):
        printChar(state[i])
    printChar("\n")
    for i in range(9,16):
        printChar(state[i])
    printChar("\n ")
    for i in range(16,18):
        printChar(state[i])
    stdout.write("\n")

    
def moveRightFlip(state):
    """Returns the state after it has been moved right-flip-left-flip."""
    
    checkStateValidity(state)
    
    b = list(state) #poor name, but I'm saving space on the next line
    return b[1] + b[7] + b[9] + b[2] + b[3] + b[0] + b[6] + b[14] + b[8] + b[16] + b[10] + b[4] + b[5] + b[12] + b[13] + b[15] + b[17] + b[11]

def moveLeftFlip(state):
    """Returns the state after it has been moved left-flip-right-flip."""
    
    checkStateValidity(state)
    
    b = list(state) #poor name, but I'm saving space on the next line
    return b[6] + b[0] + b[2] + b[4] + b[5] + b[12] + b[13] + b[7] + b[1] + b[9] + b[3] + b[11] + b[17] + b[14] + b[15] + b[8] + b[10] + b[16] 

def moveFlipRight(state):
    """Returns the state after it has been moved flip-right-flip-left."""
    
    checkStateValidity(state)
    
    b = list(state) #poor name, but I'm saving space on the next line
    return b[5] + b[0] + b[3] + b[4] + b[11] + b[12] + b[6] + b[1] + b[8] + b[2] + b[10] + b[17] + b[13] + b[14] + b[7] + b[15] + b[9] + b[16] 

def moveFlipLeft(state):
    """Returns the state after it has been moved flip-left-flip-right."""
    
    checkStateValidity(state)
    
    b = list(state) #poor name, but I'm saving space on the next line
    return b[1] + b[8] + b[2] + b[10] + b[3] + b[4] + b[0] + b[7] + b[15] + b[9] + b[16] + b[11] + b[5] + b[6] + b[13] + b[14] + b[17] + b[12]         
      

def makeMove(state, moveVal):
    if moveVal == moveDict['rightFlip']:
        return moveRightFlip(state)
    elif moveVal == moveDict['leftFlip']:
        return moveLeftFlip(state)
    elif moveVal == moveDict['flipRight']:
        return moveFlipRight(state)
    elif moveVal == moveDict['flipLeft']:
        return moveFlipLeft(state)
    else:
        raise ParameterError("moveVal parameter must have one of the values specified by moveDict") 

def evaluateDistanceHeuristic(state, target="--OOO-TR-----LG-BA"):
    checkStateValidity(state)
    try: #Want to label it as an error with the target specifically so must catch.
        checkStateValidity(target)
    except ParameterError as e:
#         raise("Parameter error on target string" + str(e))
        e.value = "Parameter error on target string. " + e.value
        raise 
    
    if state == target:
        return 0
    
    if heuristicChoice == heuristicChoiceDict['lowestCircle']:
        return evaluateDistanceLowestCircle(state,target)
    else:
        return evaluateDistanceAverageCircle(state,target)

def findLettersPos(state):
    checkStateValidity(state)
    
    aPos = state.find("A")
    bPos = state.find("B")
    tPos = state.find("T")
    lPos = state.find("L")
    rPos = state.find("R")
    gPos = state.find("G")    
    oPos1 = state.find("O")
    oPos2 = state.find("O",oPos1+1)
    oPos3 = state.find("O",oPos2+1)
    
    return (aPos, bPos, tPos, lPos, rPos, gPos), (oPos1, oPos2, oPos3)
    
    

rowListParameters = [
                     [(0,0), (6,1), (13,2), (19,3)],
                     [(-6,1), (0,0), (7,1), (13,2)],
                     [(-13,2), (-7,1), (0,0), (6,1)],
                     [(-19,3), (-13,2), (-6,1), (0,0)]
                     ]

def calcDistBlock(stateIndex, targetIndex):
    s = stateIndex
    t = targetIndex
    
    if s <= 1:
        stateRow = 0
    elif s <= 8:
        stateRow = 1
    elif s <= 15:
        stateRow = 2
    else:
        stateRow = 3
    
    if t <= 1:
        targetCol = 0
    elif t <= 8:
        targetCol = 1
    elif t <= 15:
        targetCol = 2
    else:
        targetCol = 3
    
    (a,b) = rowListParameters[stateRow][targetCol]
    return abs(s-t+a)+b
    
def evaluateDistanceLowestCircle(state,target):
    posListState, oPosListState = findLettersPos(state)
    posListTarget, oPosListTarget = findLettersPos(target)
    
    totalDistance = 0
    
    for i in range(len(posListState)):
        totalDistance += calcDistBlock(posListState[i], posListTarget[i])
    
    #Calculate O-distance by keeping the targets fixed and testing all variations of state
    oDistList  = []
    oDistList.append(calcDistBlock(oPosListState[0],oPosListTarget[0]) + calcDistBlock(oPosListState[1],oPosListTarget[1]) + calcDistBlock(oPosListState[2], oPosListTarget[2])) 
    oDistList.append(calcDistBlock(oPosListState[0],oPosListTarget[0]) + calcDistBlock(oPosListState[2],oPosListTarget[1]) + calcDistBlock(oPosListState[1], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[1],oPosListTarget[0]) + calcDistBlock(oPosListState[0],oPosListTarget[1]) + calcDistBlock(oPosListState[2], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[1],oPosListTarget[0]) + calcDistBlock(oPosListState[2],oPosListTarget[1]) + calcDistBlock(oPosListState[0], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[2],oPosListTarget[0]) + calcDistBlock(oPosListState[0],oPosListTarget[1]) + calcDistBlock(oPosListState[1], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[2],oPosListTarget[0]) + calcDistBlock(oPosListState[1],oPosListTarget[1]) + calcDistBlock(oPosListState[0], oPosListTarget[2]))
    
#    print totalDistance, min(oDistList), oDistList, oPosListState, oPosListTarget
    totalDistance += min(oDistList)
    
    return totalDistance / 9.0
        
         

def evaluateDistanceAverageCircle(state,target):
    posListState, oPosListState = findLettersPos(state)
    posListTarget, oPosListTarget = findLettersPos(target)
    
    totalDistance = 0
    
    for i in range(len(posListState)):
        totalDistance += calcDistBlock(posListState[i], posListTarget[i])
    
    #Calculate O-distance by keeping the targets fixed and testing all variations of state
    oDistList  = []
    oDistList.append(calcDistBlock(oPosListState[0],oPosListTarget[0]) + calcDistBlock(oPosListState[1],oPosListTarget[1]) + calcDistBlock(oPosListState[2], oPosListTarget[2])) 
    oDistList.append(calcDistBlock(oPosListState[0],oPosListTarget[0]) + calcDistBlock(oPosListState[2],oPosListTarget[1]) + calcDistBlock(oPosListState[1], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[1],oPosListTarget[0]) + calcDistBlock(oPosListState[0],oPosListTarget[1]) + calcDistBlock(oPosListState[2], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[1],oPosListTarget[0]) + calcDistBlock(oPosListState[2],oPosListTarget[1]) + calcDistBlock(oPosListState[0], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[2],oPosListTarget[0]) + calcDistBlock(oPosListState[0],oPosListTarget[1]) + calcDistBlock(oPosListState[1], oPosListTarget[2]))
    oDistList.append(calcDistBlock(oPosListState[2],oPosListTarget[0]) + calcDistBlock(oPosListState[1],oPosListTarget[1]) + calcDistBlock(oPosListState[0], oPosListTarget[2]))
        
    totalDistance += sum(oDistList)/6.0
    
    return totalDistance / 18.0
    
    
